//    Copyright (C) Kherty.  All rights reserved.
using System;
using System.Collections.Generic;
using OpenLS.Spreadsheet.Extensibility;
using OpenLS.Spreadsheet.StandardAddIn.Statistics;

namespace OpenLS.Spreadsheet.StandardAddIn
{
    public enum PaymentType
    {
        EndOfPeriod,
        StartOfPeriod
    }

    [AddInFunctions]
    [TreatArrayValueColumnAsDoubleArray]
    [AllowDoubleToEnumConversion]
    public static class FinancialFunctions
    {
        public static double Nominal(double rate, double npery)
        {

            if (rate <= 0 || npery < 1)
                throw new ErrorException(Errors.NumError);
            return npery * (Math.Pow(1 + rate, 1 / npery) - 1);
        }

        public static object DB(double cost, double salvage, double life, double period, [Optional(12)] int month)
        {
            if (life <= 0 || cost == 0)
                return Errors.NumError;

            double rt = 1 - Math.Pow(salvage/cost, 1/life);
            rt *= 1000;
            rt = Math.Floor(rt + 0.5)/1000;
            double dbl = cost*rt*month/12;
            if (period == 1)
                return dbl;
            for (int i = 1; i < life; i++)
            {
                if (i == period - 1)
                    return (cost - dbl)*rt;
                else
                    dbl += (cost - dbl)*rt;
            }
            return ((cost - dbl)*rt*(12 - month))/12;
        }

        public static object DDB(double cost, double salvage, double life, double period, [Optional(2d)] double factor)
        {
            if (life <= 0)
                return Errors.NumError;
            double total = 0;
            for (int i = 0; i < life - 1; i++)
            {
                double d = (cost - total)*(factor/life);
                if (period - 1 == i)
                    return d;
                else
                    total += d;
            }
            return cost - total - salvage;
        }

        public static object Irr(
            [IgnoreBooleans, IgnoreNulls, AllowStringToDoubleConversion(ReferenceOption.ArgumentOnly, Strict = true),
             IgnoreStrings(ReferenceOption.ReferenceOnly), IgnoreErrors] double[] numbers,
            [Optional(0.1d)] double guess)
        {
            if (!IsValidIrrArray(numbers))
                return Errors.NumError;
            DoubleFunction f = delegate(double x) { return NpvImp(x, numbers); };
            BrentSolver s = new BrentSolver(f);
             s.MaximalIterationCount = 10000;
            object result = s.Solve(-0.999999999, 1e12);
            return result;
        }

        private static bool IsValidIrrArray(double[] numbers)
        {
            bool hasPositive = false;
            bool hasNegative = false;
            foreach (double d in numbers)
            {
                if (d > 0)
                {
                    hasPositive = true;
                    if (hasNegative)
                        return true;
                }
                if (d < 0)
                {
                    hasNegative = true;
                    if (hasPositive)
                        return true;
                }
            }
            return false;
        }

        public static object ISPMT(double rate, int per, int nper, double pv)
        {
            if (per < 1 || per >= nper + 1)
                return Errors.NumError;
            double dbl = -pv*rate;
            return (dbl - (dbl/nper*per));
        }

        public static object MIrr([IgnoreStrings, IgnoreBooleans, IgnoreNulls] double[] values,
                                  double frate,
                                  double rrate)
        {
            if (!IsValidIrrArray(values))
                return Errors.NumError;
            double npvPos = 0;
            double npvNeg = 0;
            for (int i = 0; i < values.Length; i++)
            {
                double v = values[i];
                if (v >= 0)
                    npvPos += v/Math.Pow(1 + rrate, i);
                else
                    npvNeg += v/Math.Pow(1 + frate, i);
            }
            if (npvNeg == 0 || npvPos == 0 || rrate <= -1)
                throw new ErrorException(Errors.DivError);

            double result = Math.Pow((-npvPos*Math.Pow(1 + rrate, values.Length))/(npvNeg*(1 + rrate)), (1.0/(values.Length - 1))) - 1.0;
            return result;
        }

        public static object Npv([AllowPercentToDoubleConverter, TreatNullsAsZero] double rate,
                                 [IgnoreStrings, IgnoreBooleans, IgnoreNulls] params double[] values)
        {
            if (values.Length == 0)
                return Errors.ValueError; //\\
            if (rate == -1)
                return Errors.DivError;
            return NpvImp(rate, values);
        }

        private static double NpvImp(double rate,
                                     [IgnoreStrings, IgnoreBooleans, IgnoreNulls] IEnumerable<double> values)
        {
            double result = 0;
            double value = 1;
            if (rate == -1)
                throw new ArgumentException();
            double mul = 1/(1 + rate);
            foreach (double v in values)
            {
                value *= mul;
                result += v*value;
            }
            return result;
        }

        public static object SLN(double cost, double salvage, double life)
        {
            if (life <= 0)
                return Errors.NumError;
            return (cost - salvage)/life;
        }

        public static object SYD(double cost, double salvage, double life, double per)
        {
            if (life <= 0)
                return Errors.NumError;
            return ((cost - salvage)*(life - per + 1)*2)/(life*(life + 1));
        }

        public static object VDB(double cost, double salvage, double life, double start_period, double end_period,
                                 [Optional(2d)] double factor, [Optional(true)] bool no_witch)
        {
            double periodStart = Math.Floor(start_period);
            double periodEnd = Math.Ceiling(end_period);
            var start = (int)periodStart;
            var end = (int)periodEnd;
            double result = 0d;
            if (no_witch)
            {
                int i;
                for (i = start + 1; i <= end; i++)
                {
                    double term = vdb2(cost, salvage, life, i, factor);
                    if (i == start + 1)
                        term *= (Math.Min(end_period, periodStart + 1.0)
                                  - start_period);
                    else if (i == end)
                        term *= (end_period + 1.0 - periodEnd);
                    result += term;
                }
            }
            else
            {
                double life1 = life;
                if (start_period != Math.Floor(start_period))
                    if (factor > 1)
                    {
                        if (start_period >= life / 2)
                        {
                            double part = start_period - life / 2;
                            start_period = life / 2;
                            end_period -= part;
                            life1 += 1;
                        }
                    }
                cost -= vdb1(cost, salvage, life, life1, start_period, factor);
                result = vdb1(cost, salvage, life, life - start_period, end_period - start_period, factor);
            }
            return result;
        }

        private static double vdb1(double cost, double salvage, double life, double life1, double period, double factor)
        {
            double result = 0;
            double end2 = Math.Ceiling(period);
            var end = (int)end2;
            double residualValue = cost - salvage;
            bool b = false;
            int i;
            double v = 0;
            for (i = 1; i <= end; i++)
            {
                double term;
                if (!b)
                {
                    double g = vdb2(cost, salvage, life, i, factor);
                    v = residualValue / (life1 - (i - 1));
                    if (v > g)
                    {
                        term = v;
                        b = true;
                    }
                    else
                    {
                        term = g;
                        residualValue -= g;
                    }
                }
                else
                    term = v;

                if (i == end)
                    term *= (period + 1.0 - end2);
                result += term;
            }
            return result;
        }

        private static double vdb2(double value, double rest, double duration, double period, double factor)
        {
            double result, oldValue;
            double iRate = factor / duration;
            if (iRate >= 1.0)
            {
                iRate = 1.0;
                oldValue = period == 1.0 ? value : 0.0;
            }
            else
                oldValue = value * Math.Pow(1.0 - iRate, period - 1.0);
            double newValue = value * Math.Pow(1.0 - iRate, period);
            if (newValue < rest)
                result = oldValue - rest;
            else
                result = oldValue - newValue;
            if (result < 0.0)
                result = 0.0;
            return result;
        }

        public static object FV(double rate, double nper, double pmt, [Optional(0.0d)] double pv,
                                [Optional(PaymentType.EndOfPeriod)] PaymentType type)
        {
            return LoanFunctions.FutureValue(rate, nper, pmt, pv, type);
        }

        public static object nper(double rate, double pmt, double pv, [Optional(0.0d)] double fv,
                                  [Optional(PaymentType.EndOfPeriod)] PaymentType type)
        {
            if (rate == 0 && pmt != 0)
            {
                return -(fv + pv)/pmt;
            }
            if (rate < 0)
                return Errors.DivError;
            double tmp = (pmt*(1.0 + rate*(int) type) - fv*rate)/
                         (pv*rate + pmt*(1.0 + rate*(int) type));
            if (tmp <= 0.0)
                return Errors.ValueError;

            return (Math.Log(tmp)/Math.Log(1 + rate));
        }

        public static object IPmt(double rate, double per, double nper, double pv, [Optional(0d)] double fv,
                                  [Optional(PaymentType.EndOfPeriod)] PaymentType type)
        {
            if (per < 1 || per > nper + 1)
                return Errors.NumError;
            return LoanFunctions.InterestPayment(rate, per, nper, pv, fv, type);
        }

        public static object pv(double rate, double nper, double pmt, [Optional(0d)] double fv,
                                [Optional(PaymentType.EndOfPeriod)] PaymentType type)
        {
            try
            {
                return LoanFunctions.PresentValue(rate, nper, pmt, fv, type);
            }
            catch (DivideByZeroException)
            {
                return Errors.DivError;
            }
        }

        public static object Rate(double nper, double pmt, double pv, [ Optional(0d)] double fv,
                                  [Optional(PaymentType.EndOfPeriod)] PaymentType type, [Optional(10d)] double guess)
        {
            return LoanFunctions.Rate(nper, pmt, pv, fv, type, guess);
        }
    }
}